Identify which CLI an upgrade warning is about - #2670
Conversation
There was a problem hiding this comment.
🟡 Not ready to approve
Doctor can report false installation mismatches, omit single-install details, and hang on unresponsive executables.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
This review doesn't count toward merge requirements. Sign up for the private preview to control whether Copilot approvals count.
Pull request overview
Improves upgrade diagnostics for systems with multiple Stellar CLI installations.
Changes:
- Identifies the executable responsible for upgrade warnings and cache writes.
- Expands
doctordiagnostics to inspect PATH installations. - Adds request and shutdown timeouts for background upgrade checks.
File summaries
| File | Description |
|---|---|
cmd/soroban-cli/src/upgrade_check.rs |
Adds executable identification, cache attribution, and fetch timeout. |
cmd/soroban-cli/src/config/upgrade_check.rs |
Persists the cache writer with backward compatibility. |
cmd/soroban-cli/src/commands/doctor.rs |
Reports installations and cache-writer mismatches. |
cmd/soroban-cli/src/cli.rs |
Gives background checks a completion grace period. |
Review details
Suppressed comments (1)
cmd/soroban-cli/src/commands/doctor.rs:242
- The usual single-install case returns before printing the executable's path and version, so
doctordoes not actually list every PATH installation as promised. The zero-match case is also reported as “Only one.” Always enumerate nonempty results and handle zero separately.
if installs.len() <= 1 {
print.checkln("Only one Stellar CLI found on PATH".to_string());
return;
- Files reviewed: 4/4 changed files
- Comments generated: 3
- Review effort level: Balanced
We're testing this review assessment. Please use 👍 or 👎 to tell us if it's correct.
There was a problem hiding this comment.
🟡 Changes recommended
Some installation listings and cache-writer diagnostics are incomplete or misleading.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
This review doesn't count toward merge requirements. Sign up for the private preview to control whether Copilot approvals count.
Review details
Suppressed comments (3)
cmd/soroban-cli/src/upgrade_check.rs:140
- This assigns
last_checked_byafter a failed request without refreshing either cached version, butdoctorlater tells users the cache was “last refreshed by” this executable. That can falsely attribute version data to an install that never fetched it. Keep this assignment if the intended identity is the last writer/pacer, but update the field documentation and alldoctormessages to say “last checked/written by”; alternatively, record this field only after successful refreshes.
// A failed attempt still paces the next one, so record who
// paced it -- otherwise the file credits whichever install
// last succeeded, which may not be the one holding it back.
stats.last_checked_by = Some(check_performed_by());
cmd/soroban-cli/src/commands/doctor.rs:264
- The single-install branch does not call
list_installs, sodoctoromits that executable's PATH location and version. This contradicts the PR's stated behavior of listing every discoveredstellar/sorobanexecutable with its version; the separate “Running executable” line is not necessarily the PATH entry and has no version.
(1, _) => print.checkln("Only one Stellar CLI found on PATH".to_string()),
cmd/soroban-cli/src/commands/doctor.rs:277
common_version == Nonealso means one or more probes returnedNone, not necessarily that known versions differ. For example, two executables that cannot run are both listed as “unknown version” while this branch claims they reported different versions. Distinguish probe failures from genuinely distinct known versions so the diagnostic does not give a false cause.
(count, None) => {
print.warnln(format!(
"Found {count} Stellar CLI executables on PATH reporting different versions; \
an outdated one can report a version that disagrees with `stellar --version`:"
));
- Files reviewed: 6/6 changed files
- Comments generated: 0 new
- Review effort level: Balanced
We're testing this review assessment. Please use 👍 or 👎 to tell us if it's correct.
Motivation: Copilot's review of stellar#2670 found three `doctor` diagnostics claiming more than the data behind them supports, each able to send a user after a cause that was never observed. Behavior: - A check whose fetch failed still stamps the cache, but leaves the recorded versions untouched, so "last refreshed by" credited an install with version data it never fetched. Say "checked" instead, in the messages and the field docs: the writer paces the next check rather than vouching for the versions stored beside it. Recording it only after a successful fetch was the alternative, and it hides the install worth finding -- a stale one whose fetch fails still suppresses everyone else's check for a day. - The single-install branch printed a count without the listing, so the one case a listing would settle was the one case that omitted path and version. List every discovered install. - Absent agreement was reported as disagreement: two executables that cannot be run are both unknown, yet the message blamed differing versions. `InstalledVersions` now keeps Agreed, Disagree and Unanswered apart. An observed disagreement still wins over a failed probe alongside it, because that one is a fact. Tests: four unit cases for `summarize_versions`, and two integration cases driving real subprocesses through a fake `PATH` -- two unrunnable CLIs, and a disagreement sitting next to an unreadable binary. The lone-install listing and the reworded cache-writer lines are asserted too. Release impact: no breaking change and no migration. The `upgrade_check.json` shape is untouched and older files still load. Only `doctor`'s stderr wording changes -- no command, flag or help text does, so `FULL_HELP_DOCS.md` stands as is. Co-authored-by: Nearx-Labs <nearxlabs@nearx.com.br>
Motivation: review of stellar#2670 found the taxonomy added in b7df621 over-claiming in two places of its own -- the same class of error it exists to remove, one size smaller. Behavior: - `Disagree` counted every executable found, including ones that never answered. Two reporting different versions beside a third that could not be run printed "Found 3 Stellar CLI executables on PATH reporting different versions", contradicted by the listing directly beneath it, where the third reads "(unknown version)". It now carries how many went unanswered and names only the ones that were heard from: "the 2 that reported a version do not agree (1 could not be asked)". - `Unanswered` discarded what the answering executables established. Two at 27.1.0 beside one that cannot run is a machine whose reachable installs agree, and that was the most useful fact on the line; the message said only that agreement could not be determined. It now carries that version and leads with it, while still declining to call the whole set agreed: a version that was never read cannot be ruled out. Every count now sits next to what it counts, so the sentence can be checked against the listing below it. Also drops the redundant `return` in the zero-install arm. `list_installs` prints nothing for an empty slice, so the early exit bought nothing and only broke the symmetry between arms. Tests: unit cases pin the new payloads, including that an agreement survives a failed probe beside it and that the disagreement count excludes the executable that never answered. An integration case drives the agreement-plus-unreadable scenario through real subprocesses, and the two existing messages that changed are re-pinned. Release impact: no breaking change and no migration -- only `doctor`'s stderr wording moves, so `FULL_HELP_DOCS.md` stands as is. Co-authored-by: Nearx-Labs <nearxlabs@nearx.com.br>
Motivation: Copilot's review of stellar#2670 found three `doctor` diagnostics claiming more than the data behind them supports, each able to send a user after a cause that was never observed. Behavior: - A check whose fetch failed still stamps the cache, but leaves the recorded versions untouched, so "last refreshed by" credited an install with version data it never fetched. Say "checked" instead, in the messages and the field docs: the writer paces the next check rather than vouching for the versions stored beside it. Recording it only after a successful fetch was the alternative, and it hides the install worth finding -- a stale one whose fetch fails still suppresses everyone else's check for a day. - The single-install branch printed a count without the listing, so the one case a listing would settle was the one case that omitted path and version. List every discovered install. - Absent agreement was reported as disagreement: two executables that cannot be run are both unknown, yet the message blamed differing versions. `InstalledVersions` now keeps Agreed, Disagree and Unanswered apart. An observed disagreement still wins over a failed probe alongside it, because that one is a fact. Tests: four unit cases for `summarize_versions`, and two integration cases driving real subprocesses through a fake `PATH` -- two unrunnable CLIs, and a disagreement sitting next to an unreadable binary. The lone-install listing and the reworded cache-writer lines are asserted too. Release impact: no breaking change and no migration. The `upgrade_check.json` shape is untouched and older files still load. Only `doctor`'s stderr wording changes -- no command, flag or help text does, so `FULL_HELP_DOCS.md` stands as is. Co-authored-by: Nearx-Labs <nearxlabs@nearx.com.br>
Motivation: review of stellar#2670 found the taxonomy added in b7df621 over-claiming in two places of its own -- the same class of error it exists to remove, one size smaller. Behavior: - `Disagree` counted every executable found, including ones that never answered. Two reporting different versions beside a third that could not be run printed "Found 3 Stellar CLI executables on PATH reporting different versions", contradicted by the listing directly beneath it, where the third reads "(unknown version)". It now carries how many went unanswered and names only the ones that were heard from: "the 2 that reported a version do not agree (1 could not be asked)". - `Unanswered` discarded what the answering executables established. Two at 27.1.0 beside one that cannot run is a machine whose reachable installs agree, and that was the most useful fact on the line; the message said only that agreement could not be determined. It now carries that version and leads with it, while still declining to call the whole set agreed: a version that was never read cannot be ruled out. Every count now sits next to what it counts, so the sentence can be checked against the listing below it. Also drops the redundant `return` in the zero-install arm. `list_installs` prints nothing for an empty slice, so the early exit bought nothing and only broke the symmetry between arms. Tests: unit cases pin the new payloads, including that an agreement survives a failed probe beside it and that the disagreement count excludes the executable that never answered. An integration case drives the agreement-plus-unreadable scenario through real subprocesses, and the two existing messages that changed are re-pinned. Release impact: no breaking change and no migration -- only `doctor`'s stderr wording moves, so `FULL_HELP_DOCS.md` stands as is. Co-authored-by: Nearx-Labs <nearxlabs@nearx.com.br>
Motivation: Copilot's review of stellar#2670 found three `doctor` diagnostics claiming more than the data behind them supports, each able to send a user after a cause that was never observed. Behavior: - A check whose fetch failed still stamps the cache, but leaves the recorded versions untouched, so "last refreshed by" credited an install with version data it never fetched. Say "checked" instead, in the messages and the field docs: the writer paces the next check rather than vouching for the versions stored beside it. Recording it only after a successful fetch was the alternative, and it hides the install worth finding -- a stale one whose fetch fails still suppresses everyone else's check for a day. - The single-install branch printed a count without the listing, so the one case a listing would settle was the one case that omitted path and version. List every discovered install. - Absent agreement was reported as disagreement: two executables that cannot be run are both unknown, yet the message blamed differing versions. `InstalledVersions` now keeps Agreed, Disagree and Unanswered apart. An observed disagreement still wins over a failed probe alongside it, because that one is a fact. Tests: four unit cases for `summarize_versions`, and two integration cases driving real subprocesses through a fake `PATH` -- two unrunnable CLIs, and a disagreement sitting next to an unreadable binary. The lone-install listing and the reworded cache-writer lines are asserted too. Release impact: no breaking change and no migration. The `upgrade_check.json` shape is untouched and older files still load. Only `doctor`'s stderr wording changes -- no command, flag or help text does, so `FULL_HELP_DOCS.md` stands as is. Co-authored-by: Nearx-Labs <nearxlabs@nearx.com.br>
Motivation: review of stellar#2670 found the taxonomy added in b7df621 over-claiming in two places of its own -- the same class of error it exists to remove, one size smaller. Behavior: - `Disagree` counted every executable found, including ones that never answered. Two reporting different versions beside a third that could not be run printed "Found 3 Stellar CLI executables on PATH reporting different versions", contradicted by the listing directly beneath it, where the third reads "(unknown version)". It now carries how many went unanswered and names only the ones that were heard from: "the 2 that reported a version do not agree (1 could not be asked)". - `Unanswered` discarded what the answering executables established. Two at 27.1.0 beside one that cannot run is a machine whose reachable installs agree, and that was the most useful fact on the line; the message said only that agreement could not be determined. It now carries that version and leads with it, while still declining to call the whole set agreed: a version that was never read cannot be ruled out. Every count now sits next to what it counts, so the sentence can be checked against the listing below it. Also drops the redundant `return` in the zero-install arm. `list_installs` prints nothing for an empty slice, so the early exit bought nothing and only broke the symmetry between arms. Tests: unit cases pin the new payloads, including that an agreement survives a failed probe beside it and that the disagreement count excludes the executable that never answered. An integration case drives the agreement-plus-unreadable scenario through real subprocesses, and the two existing messages that changed are re-pinned. Release impact: no breaking change and no migration -- only `doctor`'s stderr wording moves, so `FULL_HELP_DOCS.md` stands as is. Co-authored-by: Nearx-Labs <nearxlabs@nearx.com.br>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 6 out of 6 changed files in this pull request and generated no new comments.
Suppressed comments (3)
cmd/soroban-cli/src/commands/doctor.rs:266
- A single discovered executable is always shown with a success marker, even when its version probe failed and the list immediately labels it
unknown version. This makes a broken or non-Stellar executable look healthy, whereas the same failed probe correctly warns when multiple entries exist. MatchInstalledVersions::Agreedfor the success case and warn forUnanswered.
// One file, so nothing can disagree with it. Still list it: the running
// executable is not necessarily the one `PATH` resolves by name, and the
// line above carries no version.
(1, _) => print.checkln("Only one Stellar CLI found on PATH:".to_string()),
cmd/soroban-cli/src/upgrade_check.rs:48
- Using the canonical target as the installation identity is not stable across symlink-managed upgrades. For example, Homebrew keeps
/opt/homebrew/bin/stellaras the user-facing install path but retargets it from one versioned Cellar path to another; this cache value therefore changes after a normal in-place upgrade anddoctorreports a different CLI. Preserve or separately record a stable invoked/install path for identity, while retaining the resolved path only as diagnostic detail.
let path = std::env::current_exe().ok()?;
let path = path.canonicalize().unwrap_or(path);
cmd/soroban-cli/src/commands/doctor.rs:54
- This read can race with the ordinary upgrade-check task spawned in
cli.rs:81beforeroot.run(). If the cache is stale and that task completes or fails quickly, it writes this executable aslast_checked_bybefore this load, hiding the different-install warning that this snapshot is meant to preserve. Skip the global background check fordoctor, or capture/pass the previous writer before spawning it.
This issue also appears on line 263 of the same file.
// Read this before `check_version`, which refreshes the cache and would
// otherwise record this very run as the writer -- hiding the mismatch
// the report exists to reveal.
let previous_cache_writer = version_cache_writer();
|
Addressed the three findings from the latest review, one commit each. A lone install is shown with a success marker even when its probe failed — 39bc91c The arm matched on the count alone, so a single install got a success tick above a line reading The canonical path is not stable across symlink-managed upgrades — dec74ce Agreed, and it is the same false mismatch already taken out of the version comparison, arriving by way of the path instead. Two consequences worth flagging, since neither was in the finding:
The cache-writer read races the background check spawned in Real, and there was more to it than the race:
|
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 6 out of 6 changed files in this pull request and generated no new comments.
Suppressed comments (1)
cmd/soroban-cli/src/commands/doctor.rs:208
- Comparing the exact executable paths misclassifies the two binary names from one normal installation. If
/usr/local/bin/sorobanlast wrote the shared cache and the user runs/usr/local/bin/stellar doctor, this branch warns about a “different Stellar CLI” even when both binaries came from the same install and version;check_installsexplicitly recognizes that one install normally ships both names. Normalize knownstellar/sorobansibling paths to a shared installation identity (while retaining the executable/version for diagnostics), and cover the cross-name same-install case.
match (&writer.executable, &this_cli.executable) {
(Some(written_by), Some(running)) if written_by == running => {
|
Ran an adversarial review pass on this branch and pushed a follow-up commit fixing three gaps it surfaced (a6af472):
All existing + new tests pass (10 unit + 14 Two lower-severity items from the review were left as follow-ups rather than blocking this PR:
|
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 6 out of 6 changed files in this pull request and generated no new comments.
Suppressed comments (2)
cmd/soroban-cli/src/cli.rs:133
- The PR description promises a 2-second grace period and calls out at most 2 seconds of added latency, but this computes 6 seconds (
FETCH_TIMEOUTis 5 seconds). A fast first command can therefore block three times longer than the documented behavior. Either retain the stated 2-second cap or update the intended contract and PR description to the longer delay.
const UPGRADE_CHECK_GRACE: std::time::Duration =
FETCH_TIMEOUT.saturating_add(std::time::Duration::from_secs(1));
cmd/soroban-cli/src/commands/doctor.rs:477
- The timeout drops the
Command::output()future, but Tokio processes default tokill_on_drop(false), so a timed-out probe keeps running and is not reaped. Repeateddoctorruns can therefore accumulate stalledstellar/sorobanprocesses. Configure the child to be killed when the timed-out future is dropped (or explicitly spawn, kill, and wait for it).
let output = tokio::time::timeout(
INSTALL_PROBE_TIMEOUT,
tokio::process::Command::new(path).args(args).output(),
)
The warning names a version but not the install it came from, and on a machine with more than one Stellar CLI those are different questions. A stale `soroban` left in `~/.cargo/bin` by an old `cargo install`, or a Homebrew install shadowed by a newer one, prints "a new release is available: 22.1.0 -> 23.3.0" using its own version -- and the user compares it against `stellar --version`, which answers 25.2.0. The numbers disagree because two binaries are talking, which the message gives no way to see. It now names the executable it is about. The reported latest version could also be stale on its own. The check runs in a background task that `main` drops on return, so a command finishing faster than the request to crates.io left the fetched versions unwritten and the next run starting over -- re-fetching each time while continuing to report whatever the cache already held. The task now gets a grace period to land its result. That grace has to outlast the fetch it waits on, so the fetch needed a bound of its own: the shared HTTP client only limits how long connecting may take, so a server that accepts and then stalls could hang the request indefinitely, and no grace period can be chosen against a wait with no ceiling. With the fetch capped at 5s the grace is that plus a second, which cannot expire before a fetch its own timeout allowed to succeed. The cost is on the first command of the day, and only when the check actually goes to the network: up to 6s, where the previous behaviour was to drop the check. `STELLAR_NO_UPDATE_CHECK` still turns it off entirely. This does not reach warnings printed by already-released binaries -- an old `soroban` will keep printing its old unannotated message, since the fix has to be in the binary doing the printing. What it fixes is every warning from this release forward. Co-authored-by: Nearx-Labs <nearxlabs@nearx.com.br>
finish_upgrade_check awaited the background task's grace period after root.run(), but every branch of the error handling above it exits via std::process::exit, which ends the process without running anything placed after it. Any command that returned an error -- a bad flag, a failed simulation, an unreachable RPC -- skipped the grace period entirely and killed the check before it could write its result to the cache, reproducing the exact bug this was meant to fix. Capturing root.run()'s result first and finishing the check before acting on it means every exit path waits on it once, not only the success path. Co-authored-by: Nearx-Labs <nearxlabs@nearx.com.br>
What problem does your feature solve?
The upgrade warning names a version but not the install it came from, and on a machine with more than one Stellar CLI those are different questions.
A stale
sorobanleft in~/.cargo/binby an oldcargo install, or a Homebrew install shadowed by a newer one, printsA new release of Stellar CLI is available: 22.1.0 -> 23.3.0using its own version. The user compares that againststellar --version, which answers25.2.0. The numbers disagree because two different binaries are talking — and the message gives no way to see that.The reported latest version could also be stale on its own. The check runs in a background task that
maindrops on return, so a command finishing faster than the request to crates.io left the fetched versions unwritten and the next run starting over — re-fetching every time while continuing to report whatever the cache already held.What would you like to see?
What alternatives are there?
Accepting the known limitations:
sorobanwill keep printing its old unannotated message — the fix has to live in the binary doing the printing. What it fixes is every warning from this release forward.process::exit.STELLAR_NO_UPDATE_CHECKstill disables it.Diagnostics, split out
An earlier revision of this PR also taught
stellar doctorto list everystellar/sorobanexecutable onPATHwith its version, and to report which install last wrote the shared version cache. That is diagnosis rather than fix — useful for the case above that no code change can reach — and it was ~90% of the diff. It has been split out so this PR stays reviewable, and will follow separately.fix: #2464